16. 缺失值-英国零售商数据处理

⭐ 本章学习目标

  • 理解缺失值的三种缺失机制(MCAR、MAR、MNAR)
  • 掌握缺失值的检测与统计方法
  • 学会使用删除法与填充法处理缺失值
  • 完成英国零售商数据的缺失值清洗实战

⭐ 什么是缺失值?

  • 缺失值(Missing Values) 是数据质量问题中最常见的挑战
  • 在真实数据分析中,几乎所有数据集都会包含缺失值
  • 错误处理缺失值会导致分析结果失真、决策偏差

常见产生原因:

  • 数据录入遗漏或系统故障
  • API采集失败、存储损坏
  • 业务逻辑导致(如停牌、未营业)

⭐ 三种缺失机制

缺失类型 英文缩写 含义
完全随机缺失 MCAR 缺失与任何变量无关
随机缺失 MAR 缺失仅依赖已观测数据
非随机缺失 MNAR 缺失与缺失值本身相关

理解缺失机制是选择处理策略的前提。

⭐ 错误处理的后果

  • 有偏估计: 删除MNAR数据会系统性扭曲样本
  • 信息损失: 过度删除会减少样本量,降低统计功效
  • 虚假结论: 不当填充可能创造不存在的模式

⭐ 平台任务解答代码

Listing 1
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 注:英国零售商data.csv数据文件本地没有,但平台已经内置
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
path= '英国零售商data.csv'  # 设置path为"英国零售商data.csv"
df=pd.read_csv(path,dtype={'CustomerID':str,'InvoiceID':str})  # 从CSV文件读取数据存入df
# 剔除CustomerID的缺失数据
df.dropna(subset=['CustomerID'],inplace=True)
print(df.CustomerID.count())  # 输出非缺失值计数

⭐ 缺失值检测:创建示例数据

Listing 2
import pandas as pd
import numpy as np

# 6家零售门店(A-F)的日销售数据
data = {
    'Store': ['A', 'B', 'C', 'D', 'E', 'F'],
    # 门店B和E的销售额缺失(系统故障或未营业)
    'Sales': [100, np.nan, 150, 200, np.nan, 180],
    # 门店C和F的利润缺失(成本数据未录入)
    'Profit': [10, 15, np.nan, 20, 25, np.nan],
    # 门店D的客户数缺失(客流计数器故障)
    'Customers': [50, 75, 100, np.nan, 80, 95]
}
df = pd.DataFrame(data)
print('原始数据:')
print(df)
原始数据:
  Store  Sales  Profit  Customers
0     A  100.0    10.0       50.0
1     B    NaN    15.0       75.0
2     C  150.0     NaN      100.0
3     D  200.0    20.0        NaN
4     E    NaN    25.0       80.0
5     F  180.0     NaN       95.0

⭐ 检测方法1:isnull() 识别缺失位置

Listing 3
# df.isnull():返回布尔型数据框
# True表示该位置是缺失值,False表示不是
print('缺失值位置(True表示缺失):')
print(df.isnull())
缺失值位置(True表示缺失):
   Store  Sales  Profit  Customers
0  False  False   False      False
1  False   True   False      False
2  False  False    True      False
3  False  False   False       True
4  False   True   False      False
5  False  False    True      False
  • isnull()isna() 功能完全相同,可互换使用
  • 返回与原数据框同形状的布尔矩阵

⭐ 检测方法2:统计每列缺失值数量

Listing 4
# isnull()返回布尔值(True=1, False=0),sum()将True值相加
print('各列缺失值数量:')
print(df.isnull().sum())
各列缺失值数量:
Store        0
Sales        2
Profit       2
Customers    1
dtype: int64
  • 这是评估数据质量的关键指标
  • 可快速识别哪些变量问题最严重

⭐ 检测方法3:缺失值比例与完整行数

Listing 5
# 计算每列缺失值占总行数的百分比
print('各列缺失值比例(%):')
print(df.isnull().mean() * 100)
print()

# 完整行数(无任何缺失的行)
print('完整行数(无缺失):', df.dropna().shape[0])
print('总行数:', df.shape[0])
各列缺失值比例(%):
Store         0.000000
Sales        33.333333
Profit       33.333333
Customers    16.666667
dtype: float64

完整行数(无缺失): 1
总行数: 6

决策阈值参考:

  • 缺失率 < 5%:可简单删除或填充
  • 缺失率 5%~20%:需谨慎选择策略
  • 缺失率 > 20%:考虑删除变量或高级插补

⭐ 缺失模式可视化:创建模拟数据

Listing 6
import pandas as pd
import numpy as np

np.random.seed(42)
n_rows = 100
data_vis = pd.DataFrame({
    'A': np.random.rand(n_rows),
    'B': np.random.rand(n_rows),
    'C': np.random.rand(n_rows),
    'D': np.random.rand(n_rows)
})

# 场景1:变量A的10%随机缺失(MCAR)
mask_a = np.random.rand(n_rows) < 0.1
data_vis.loc[mask_a, 'A'] = np.nan

# 场景2:变量B的条件缺失(MAR:当A<0.5时B缺失)
data_vis.loc[data_vis['A'] < 0.5, 'B'] = np.nan

# 场景3:变量C的15%随机缺失
mask_c = np.random.rand(n_rows) < 0.15
data_vis.loc[mask_c, 'C'] = np.nan

# 场景4:变量D无缺失(对照组)
missing_matrix = data_vis.isnull()
print('各变量缺失值数量:')
print(missing_matrix.sum())
各变量缺失值数量:
A    16
B    43
C    15
D     0
dtype: int64

⭐ 缺失模式热力图与条形图

import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 子图1:缺失模式热力图(红色=缺失)
axes[0].imshow(missing_matrix.T, aspect='auto', cmap='Reds', interpolation='none')
axes[0].set_yticks(range(len(data_vis.columns)))
axes[0].set_yticklabels(data_vis.columns)
axes[0].set_xlabel('观测序号')
axes[0].set_title('缺失模式热力图(红色=缺失)')
axes[0].grid(False)

# 子图2:缺失比例条形图
missing_ratios = missing_matrix.sum() / len(data_vis) * 100
axes[1].bar(range(len(data_vis.columns)), missing_ratios,
           color='steelblue', edgecolor='black')
axes[1].set_xticks(range(len(data_vis.columns)))
axes[1].set_xticklabels(data_vis.columns)
axes[1].set_ylabel('缺失比例(%)')
axes[1].set_title('各变量缺失比例')
axes[1].grid(axis='y', alpha=0.3)

for i, v in enumerate(missing_ratios):
    axes[1].text(i, v + 1, f'{v:.1f}%', ha='center', fontsize=10)

plt.tight_layout()
plt.show()
Figure 1: 缺失模式热力图与各变量缺失比例

⭐ 删除法概述

删除法(Deletion) 是最简单的缺失值处理方式。

适用条件:

  • 数据缺失是完全随机的(MCAR)
  • 删除后样本量仍充足
  • 缺失比例较低(通常 < 5%)

风险提示: 在时间序列中直接删除可能导致时间不连续、幸存者偏差。

⭐ 删除策略1:删除含任何缺失值的行

Listing 7
import pandas as pd
import numpy as np

data = {
    'Store': ['A', 'B', 'C', 'D', 'E'],
    'Sales': [100, np.nan, 150, 200, np.nan],
    'Profit': [10, 15, np.nan, 20, 25]
}
df = pd.DataFrame(data)
print('原始数据:')
print(df)
print()

# how='any':只要行中有任何一个缺失值,就删除该行
df_drop_any = df.dropna(how='any')
print('删除含任何缺失值的行(how="any"):')
print(df_drop_any)
原始数据:
  Store  Sales  Profit
0     A  100.0    10.0
1     B    NaN    15.0
2     C  150.0     NaN
3     D  200.0    20.0
4     E    NaN    25.0

删除含任何缺失值的行(how="any"):
  Store  Sales  Profit
0     A  100.0    10.0
3     D  200.0    20.0

⭐ 删除策略2:只删除全部缺失的行

Listing 8
# how='all':只有当行中所有值都缺失时,才删除
df_drop_all = df.dropna(how='all')
print('只删除全部缺失的行(how="all"):')
print(df_drop_all)
只删除全部缺失的行(how="all"):
  Store  Sales  Profit
0     A  100.0    10.0
1     B    NaN    15.0
2     C  150.0     NaN
3     D  200.0    20.0
4     E    NaN    25.0
  • 这是一种宽松策略,保留部分数据
  • 只有整行全部缺失时才会被删除

⭐ 删除策略3:按特定列删除

Listing 9
# subset=['Sales']:只检查Sales列是否缺失
df_drop_subset = df.dropna(subset=['Sales'])
print('只删除Sales列缺失的行:')
print(df_drop_subset)
只删除Sales列缺失的行:
  Store  Sales  Profit
0     A  100.0    10.0
2     C  150.0     NaN
3     D  200.0    20.0
  • 核心变量缺失时,整行数据可能无意义
  • 其他列缺失不影响删除决策

⭐ 删除策略4:删除含缺失值的列

Listing 10
# axis=1:按列操作(删除包含缺失值的列)
df_drop_cols = df.dropna(axis=1)
print('删除含缺失值的列:')
print(df_drop_cols)
删除含缺失值的列:
  Store
0     A
1     B
2     C
3     D
4     E
  • 当某列缺失率太高(如 >30%),可考虑删除该列
  • 注意:会丢失该变量的所有信息

⭐ 填充法概述

填充法(Imputation) 用估计值替代缺失值,保留所有观测。

方法 适用场景 优缺点
零填充 计数类数据 简单但通常不推荐
均值填充 横截面数据 低估方差
中位数填充 含极端值数据 对异常值稳健
前向填充 时间序列 保持状态持续
后向填充 时间序列 可能引入前视偏差
线性插值 高频数据 假设平滑变化

⭐ 创建金融时间序列示例

Listing 11
import pandas as pd
import numpy as np

# 10天的价格序列,含3个缺失值
dates = pd.date_range('2024-01-01', periods=10)
prices = [100.5, np.nan, 101.2, 100.8, np.nan, np.nan, 102.5, 103.0, np.nan, 104.2]
df_prices = pd.DataFrame({'Date': dates, 'Price': prices})
print('原始价格数据:')
print(df_prices)
原始价格数据:
        Date  Price
0 2024-01-01  100.5
1 2024-01-02    NaN
2 2024-01-03  101.2
3 2024-01-04  100.8
4 2024-01-05    NaN
5 2024-01-06    NaN
6 2024-01-07  102.5
7 2024-01-08  103.0
8 2024-01-09    NaN
9 2024-01-10  104.2

⭐ 填充策略1:零填充与均值填充

Listing 12
# 策略1:零填充(通常不推荐,价格不可能是0)
df_zero = df_prices.copy()
df_zero['Price'] = df_zero['Price'].fillna(0)
print('零填充(通常不推荐):')
print(df_zero['Price'].values)
print()

# 策略2:均值填充
mean_price = df_prices['Price'].mean()
df_mean = df_prices.copy()
df_mean['Price'] = df_mean['Price'].fillna(mean_price)
print(f'均值填充(均值={mean_price:.2f}):')
print(df_mean['Price'].values)
零填充(通常不推荐):
[100.5   0.  101.2 100.8   0.    0.  102.5 103.    0.  104.2]

均值填充(均值=102.03):
[100.5        102.03333333 101.2        100.8        102.03333333
 102.03333333 102.5        103.         102.03333333 104.2       ]

⭐ 填充策略2:中位数填充

Listing 13
# 中位数比均值更稳健,不受极端值影响
median_price = df_prices['Price'].median()
df_median = df_prices.copy()
df_median['Price'] = df_median['Price'].fillna(median_price)
print(f'中位数填充(中位数={median_price:.2f}):')
print(df_median['Price'].values)
中位数填充(中位数=101.85):
[100.5  101.85 101.2  100.8  101.85 101.85 102.5  103.   101.85 104.2 ]
  • 中位数不受极端值(如涨跌停价格)影响
  • 适用于数据分布有偏的场景

⭐ 填充策略3:前向填充(ffill)

Listing 14
# method='ffill':用前一个有效值填充
df_ffill = df_prices.copy()
df_ffill['Price'] = df_ffill['Price'].fillna(method='ffill')
print('前向填充(用前一个值填充):')
print(df_ffill[['Date', 'Price']])
前向填充(用前一个值填充):
        Date  Price
0 2024-01-01  100.5
1 2024-01-02  100.5
2 2024-01-03  101.2
3 2024-01-04  100.8
4 2024-01-05  100.8
5 2024-01-06  100.8
6 2024-01-07  102.5
7 2024-01-08  103.0
8 2024-01-09  103.0
9 2024-01-10  104.2
  • 金融应用: 填充停牌期间的价格(假设价格保持不变)
  • 复牌时收益率包含停牌期间全部变化

⭐ 填充策略4:后向填充(bfill)

Listing 15
# method='bfill':用后一个有效值填充
df_bfill = df_prices.copy()
df_bfill['Price'] = df_bfill['Price'].fillna(method='bfill')
print('后向填充(用后一个值填充):')
print(df_bfill[['Date', 'Price']])
后向填充(用后一个值填充):
        Date  Price
0 2024-01-01  100.5
1 2024-01-02  101.2
2 2024-01-03  101.2
3 2024-01-04  100.8
4 2024-01-05  102.5
5 2024-01-06  102.5
6 2024-01-07  102.5
7 2024-01-08  103.0
8 2024-01-09  104.2
9 2024-01-10  104.2
  • 使用了「未来」的信息,可能导致 前视偏差
  • 在金融数据中较少使用

⭐ 填充策略5:线性插值

Listing 16
# interpolate(method='linear'):在两个已知点之间线性插值
df_linear = df_prices.copy()
df_linear['Price'] = df_linear['Price'].interpolate(method='linear')
print('线性插值:')
print(df_linear[['Date', 'Price']])
线性插值:
        Date       Price
0 2024-01-01  100.500000
1 2024-01-02  100.850000
2 2024-01-03  101.200000
3 2024-01-04  100.800000
4 2024-01-05  101.366667
5 2024-01-06  101.933333
6 2024-01-07  102.500000
7 2024-01-08  103.000000
8 2024-01-09  103.600000
9 2024-01-10  104.200000
  • 假设两个已知点之间变化是线性
  • 适用于高频数据的小缺口填充

⭐ 线性插值的数学公式

对于时间点 \(t_1\)\(t_3\) 之间的缺失值 \(t_2\)

\[ y_{t_2} = y_{t_1} + \frac{t_2 - t_1}{t_3 - t_1} (y_{t_3} - y_{t_1}) \]

  • 等价于在两个已知点之间画一条直线
  • 假设价格在缺失期间平滑变化

⭐ 高级填充:时间插值与多项式插值

Listing 17
import pandas as pd
import numpy as np

np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=50)
prices = 100 + np.random.randn(50).cumsum()
prices[10:15] = np.nan  # 模拟连续5天停牌
prices[30:32] = np.nan  # 模拟连续2天缺失
prices[45] = np.nan      # 模拟单日缺失

df = pd.DataFrame({'Date': dates, 'Price': prices})
df.set_index('Date', inplace=True)

# 策略1:时间插值(考虑日期间隔)
df_time = df.interpolate(method='time')

# 策略2:多项式插值(二阶)
df_poly = df.interpolate(method='polynomial', order=2)

print('时间插值结果(第10-15行):')
print(df_time.iloc[9:16])
print()
print('多项式插值结果(第10-15行):')
print(df_poly.iloc[9:16])
时间插值结果(第10-15行):
                 Price
Date                  
2024-01-10  104.480611
2024-01-11  103.665999
2024-01-12  102.851388
2024-01-13  102.036776
2024-01-14  101.222164
2024-01-15  100.407552
2024-01-16   99.592940

多项式插值结果(第10-15行):
                 Price
Date                  
2024-01-10  104.480611
2024-01-11  104.716334
2024-01-12  104.353075
2024-01-13  103.390834
2024-01-14  102.127691
2024-01-15  100.861727
2024-01-16   99.592940

⭐ 高级填充:滚动均值填充

Listing 18
# 用前5天的平均价格填充缺失值
df_rolling = df.fillna(df.rolling(window=5, min_periods=1).mean())
print('滚动均值填充(窗口=5):')
print(df_rolling.iloc[9:16])
滚动均值填充(窗口=5):
                 Price
Date                  
2024-01-10  104.480611
2024-01-11  104.116570
2024-01-12  104.275396
2024-01-13  104.209331
2024-01-14  104.480611
2024-01-15         NaN
2024-01-16   99.592940
  • 考虑局部趋势,比全局均值更合理
  • window=5:使用前5个数据点计算均值
  • min_periods=1:最少需要1个非缺失值

⭐ 高级填充:分组填充

Listing 19
import pandas as pd
import numpy as np

stocks = pd.DataFrame({
    'Date': list(pd.date_range('2024-01-01', periods=5)) * 2,
    'Stock': ['A'] * 5 + ['B'] * 5,
    'Price': [100, 101, np.nan, 103, 104,
              50, np.nan, 52, 53, np.nan]
})
print('原始多股票数据:')
print(stocks)
print()

# 按股票代码分组,分别前向填充
stocks['Price'] = stocks.groupby('Stock')['Price'].fillna(method='ffill')
print('分组前向填充:')
print(stocks)
原始多股票数据:
        Date Stock  Price
0 2024-01-01     A  100.0
1 2024-01-02     A  101.0
2 2024-01-03     A    NaN
3 2024-01-04     A  103.0
4 2024-01-05     A  104.0
5 2024-01-01     B   50.0
6 2024-01-02     B    NaN
7 2024-01-03     B   52.0
8 2024-01-04     B   53.0
9 2024-01-05     B    NaN

分组前向填充:
        Date Stock  Price
0 2024-01-01     A  100.0
1 2024-01-02     A  101.0
2 2024-01-03     A  101.0
3 2024-01-04     A  103.0
4 2024-01-05     A  104.0
5 2024-01-01     B   50.0
6 2024-01-02     B   50.0
7 2024-01-03     B   52.0
8 2024-01-04     B   53.0
9 2024-01-05     B   53.0
  • 关键: 不同股票的数据不能混用
  • 股票A的缺失值只能用股票A的数据填充

⭐ 实战案例:英国零售商数据加载

Listing 20
import pandas as pd
import numpy as np

# 模拟英国零售商数据(演示用)
np.random.seed(42)
n_records = 1000

df_retail = pd.DataFrame({
    'InvoiceID': [f'INV{i:06d}' for i in range(n_records)],
    'CustomerID': [f'CUST{np.random.randint(1, 201):05d}'
                   if np.random.rand() > 0.1 else np.nan
                   for _ in range(n_records)],
    'InvoiceDate': pd.date_range('2023-01-01', periods=n_records),
    'Quantity': np.random.randint(1, 100, n_records),
    'UnitPrice': np.random.uniform(1, 50, n_records),
    'Country': ['UK'] * n_records
})

# 人为引入缺失值(模拟真实场景)
df_retail.loc[np.random.choice(n_records, 50), 'CustomerID'] = np.nan
df_retail.loc[np.random.choice(n_records, 30), 'Quantity'] = np.nan
df_retail.loc[np.random.choice(n_records, 20), 'UnitPrice'] = np.nan

print('数据形状:', df_retail.shape)
print('\n缺失值统计:')
print(df_retail.isnull().sum())
print('\n缺失值比例(%):')
print(df_retail.isnull().mean() * 100)
数据形状: (1000, 6)

缺失值统计:
InvoiceID        0
CustomerID     142
InvoiceDate      0
Quantity        30
UnitPrice       20
Country          0
dtype: int64

缺失值比例(%):
InvoiceID       0.0
CustomerID     14.2
InvoiceDate     0.0
Quantity        3.0
UnitPrice       2.0
Country         0.0
dtype: float64

⭐ 实战:处理 CustomerID 缺失

Listing 21
# 复制数据,保留原始数据
df_clean = df_retail.copy()

# 决策:删除CustomerID缺失的行
# 理由:客户ID是关联交易的关键字段,无法合理推测
before_customer = len(df_clean)
df_clean.dropna(subset=['CustomerID'], inplace=True)
after_customer = len(df_clean)

print(f'CustomerID处理:')
print(f'  删除前: {before_customer} 行')
print(f'  删除后: {after_customer} 行')
print(f'  删除比例: {(before_customer - after_customer) / before_customer:.2%}')
CustomerID处理:
  删除前: 1000 行
  删除后: 858 行
  删除比例: 14.20%

⭐ 实战:处理 Quantity 缺失

Listing 22
# 决策:用中位数填充(稳健,不受异常值影响)
median_quantity = df_clean['Quantity'].median()
df_clean['Quantity'] = df_clean['Quantity'].fillna(median_quantity)

print(f'Quantity处理:')
print(f'  填充值(中位数): {median_quantity}')
print(f'  剩余缺失: {df_clean["Quantity"].isnull().sum()}')
Quantity处理:
  填充值(中位数): 47.0
  剩余缺失: 0
  • 数量可能有极端值(大额批发),中位数更稳健
  • 用「典型订单量」替代缺失值

⭐ 实战:处理 UnitPrice 缺失

Listing 23
# 决策:按CustomerID分组填充均值
# 理由:同一客户购买相似价位商品
df_clean['UnitPrice'] = df_clean.groupby('CustomerID')['UnitPrice'].transform(
    lambda x: x.fillna(x.mean())
)
# 剩余缺失用全局均值填充
df_clean['UnitPrice'] = df_clean['UnitPrice'].fillna(df_clean['UnitPrice'].mean())

print(f'UnitPrice处理:')
print(f'  分组均值填充')
print(f'  剩余缺失: {df_clean["UnitPrice"].isnull().sum()}')
UnitPrice处理:
  分组均值填充
  剩余缺失: 0

⭐ 实战:最终数据质量验证

Listing 24
print('=== 处理后数据质量 ===')
print(f'总行数: {len(df_clean)}')
print(f'完整行数: {df_clean.dropna().shape[0]}')
print(f'\n最终缺失值统计:')
print(df_clean.isnull().sum())
=== 处理后数据质量 ===
总行数: 858
完整行数: 858

最终缺失值统计:
InvoiceID      0
CustomerID     0
InvoiceDate    0
Quantity       0
UnitPrice      0
Country        0
dtype: int64

⭐ 数据质量报告

Listing 25
print('=== 数据质量报告 ===\n')
print(f'总记录数: {len(df_clean)}')
print(f'唯一客户数: {df_clean["CustomerID"].nunique()}')
print(f'时间范围: {df_clean["InvoiceDate"].min()}{df_clean["InvoiceDate"].max()}')
print()

# 数值变量统计
print('数值变量统计:')
print(df_clean[['Quantity', 'UnitPrice']].describe())
print()

# 计算交易金额
df_clean['TotalAmount'] = df_clean['Quantity'] * df_clean['UnitPrice']
print(f'总交易额: £{df_clean["TotalAmount"].sum():,.2f}')
print(f'平均交易额: £{df_clean["TotalAmount"].mean():.2f}')
=== 数据质量报告 ===

总记录数: 858
唯一客户数: 197
时间范围: 2023-01-01 00:00:00 至 2025-09-26 00:00:00

数值变量统计:
         Quantity   UnitPrice
count  858.000000  858.000000
mean    48.376457   25.164971
std     28.825743   13.929409
min      1.000000    1.000570
25%     23.000000   14.123569
50%     47.000000   25.190335
75%     73.000000   36.727144
max     99.000000   49.893222

总交易额: £1,037,901.47
平均交易额: £1209.68

⭐ Top 5 客户分析

Listing 26
# 按客户分组统计
customer_stats = df_clean.groupby('CustomerID').agg({
    'TotalAmount': 'sum',
    'InvoiceID': 'count'
}).rename(columns={'InvoiceID': 'TransactionCount'})

print('交易金额前5名客户:')
print(customer_stats.nlargest(5, 'TotalAmount'))
交易金额前5名客户:
             TotalAmount  TransactionCount
CustomerID                                
CUST00054   17711.334346                 8
CUST00013   15781.737700                 7
CUST00190   14308.500800                 9
CUST00039   13121.680522                11
CUST00035   13062.162477                10

⭐ 金融案例:股票停牌数据处理

Listing 27
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=30)
prices_normal = 100 + np.random.randn(30).cumsum() * 0.5

# 创建停牌缺失
prices_suspended = prices_normal.copy()
prices_suspended[10:15] = np.nan  # 第一次停牌5天
prices_suspended[22:24] = np.nan  # 第二次停牌2天

df_stock = pd.DataFrame({
    'Date': dates,
    'Price': prices_suspended,
    'Price_Normal': prices_normal
})

print(f'缺失天数: {df_stock["Price"].isnull().sum()}')
print(f'缺失比例: {df_stock["Price"].isnull().mean():.2%}')
缺失天数: 7
缺失比例: 23.33%

⭐ 停牌处理:不同策略对比

# 不同填充策略
df_stock['ForwardFill'] = df_stock['Price'].fillna(method='ffill')
df_stock['LinearInterp'] = df_stock['Price'].interpolate(method='linear')

# 计算日收益率
df_stock['Return_Normal'] = df_stock['Price_Normal'].pct_change()
df_stock['Return_FF'] = df_stock['ForwardFill'].pct_change()
df_stock['Return_Lin'] = df_stock['LinearInterp'].pct_change()

fig, axes = plt.subplots(2, 1, figsize=(14, 10))

# 价格对比
axes[0].plot(df_stock['Date'], df_stock['Price_Normal'], 'g-',
            label='真实价格', linewidth=2)
axes[0].plot(df_stock['Date'], df_stock['ForwardFill'], 'b--',
            label='前向填充', linewidth=1.5)
axes[0].plot(df_stock['Date'], df_stock['LinearInterp'], 'r--',
            label='线性插值', linewidth=1.5, alpha=0.7)
axes[0].scatter(df_stock[df_stock['Price'].isnull()]['Date'],
               df_stock['Price_Normal'][df_stock['Price'].isnull()],
               color='red', s=50, zorder=5, label='停牌期间真实价格')
axes[0].set_ylabel('价格', fontsize=12)
axes[0].set_title('股票停牌数据处理:价格对比', fontsize=14)
axes[0].legend(fontsize=11)
axes[0].grid(True, alpha=0.3)

# 收益率对比
axes[1].plot(df_stock['Date'], df_stock['Return_Normal'], 'g-',
            label='真实收益率', linewidth=2)
axes[1].plot(df_stock['Date'], df_stock['Return_FF'], 'b--',
            label='前向填充收益率', linewidth=1.5)
axes[1].plot(df_stock['Date'], df_stock['Return_Lin'], 'r--',
            label='线性插值收益率', linewidth=1.5, alpha=0.7)
axes[1].axhline(y=0, color='k', linestyle='-', linewidth=0.5)
axes[1].set_xlabel('日期', fontsize=12)
axes[1].set_ylabel('日收益率', fontsize=12)
axes[1].set_title('股票停牌数据处理:收益率对比', fontsize=14)
axes[1].legend(fontsize=11)
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
Figure 2: 股票停牌数据处理:价格与收益率对比

⭐ 停牌处理:误差评估

Listing 28
# 计算均方误差(MSE)
mse_ff = ((df_stock['ForwardFill'] - df_stock['Price_Normal']) ** 2).mean()
mse_lin = ((df_stock['LinearInterp'] - df_stock['Price_Normal']) ** 2).mean()

print('填充误差评估:')
print(f'前向填充 MSE: {mse_ff:.4f}')
print(f'线性插值 MSE: {mse_lin:.4f}')
print(f'\nMSE越小,填充效果越好。')
填充误差评估:
前向填充 MSE: 0.2406
线性插值 MSE: 0.0391

MSE越小,填充效果越好。

⭐ 停牌处理最佳实践

数据类型 推荐方法 理由
价格序列 前向填充 假设停牌期间价格不变
收益率序列 设为 0 或 NaN 停牌期间无收益
成交量序列 设为 0 停牌无交易

关键: 保留「停牌」标识,在计算统计量时排除停牌数据。

⭐ 本章小结

  • 检测方法: isnull()isnull().sum()isnull().mean()
  • 删除法: dropna(how=, subset=, axis=)
  • 填充法: fillna()interpolate()
  • 高级方法: 滚动均值填充、分组填充
  • 核心原则: 根据缺失机制和业务逻辑选择策略